﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using NetTopologySuite.Geometries;
using SharpMap.Converters.NTS;
using SharpMap.Data;
using EGIS.ShapeFileLib;
using System.ComponentModel;
using System.Runtime.Remoting.Messaging;

namespace GauSmart.core
{
    public enum DistanceType
    {
        POLYGON,
        CENTROID,
        BOUNDINGBOX
    }

    public class Engine
    {
        private readonly List<Criteria> _criterias; 
        private readonly List<SharpMap.Data.Providers.ShapeFile> _criteriaShapeList;
        private readonly SharpMap.Data.Providers.ShapeFile _potentialShape;
        private readonly DistanceType _distanceType;

        private readonly object _sync = new object();

        private List<Criteria> _computedCriteriaList;
        private List<ISuitability> _computedSuitabilityList;
        private List<ISuitability> _resultSuitabilityList;

        private bool _hasPrepared;
        private bool _hasEvaluated;
        private bool _hasResult;
        private bool _isEvaluationRunning;

        private delegate List<SuitabilityType> SuitabilityEvaluation(Range[] ranges, List<double> results);
        private delegate void GetResultDelegate(List<Weight> weightList);
        public delegate void GetResultCompletedEventHandler(object sender, GetResultAsyncCompletedEventArgs args);

        public event GetResultCompletedEventHandler GetResultCompleted;

        public bool HasResult
        {
            get { return _hasResult; }
        }

        public bool IsBusy
        {
            get { return _isEvaluationRunning; }
        }

        public Engine(List<Criteria> criterias, List<SharpMap.Data.Providers.ShapeFile> criteriaShapeList,
            SharpMap.Data.Providers.ShapeFile potenitalShape, DistanceType distanceType)
        {
            _criterias = criterias;
            _criteriaShapeList = criteriaShapeList;
            _potentialShape = potenitalShape;
            _distanceType = distanceType;
            _hasPrepared = false;
            _hasEvaluated = false;
            _hasResult = false;
        }

        public void ExtractResultMap(string dirName, string shpFileName)
        {
            DbfFieldDesc[] fields = new[] 
            { 
                new DbfFieldDesc { FieldName = "Suit", FieldType = DbfFieldType.Character, FieldLength = 50, RecordOffset = 1 }
            };

            ShapeFileWriter shpWriter = ShapeFileWriter.CreateWriter(dirName, shpFileName, EGIS.ShapeFileLib.ShapeType.Polygon, fields);

            FeatureDataSet dataSet = new FeatureDataSet();
            _potentialShape.ExecuteIntersectionQuery(_potentialShape.GetExtents(), dataSet);

            FeatureDataTable table = dataSet.Tables[0];

            for (int i = 0; i < table.Count; i++)
            {
                SharpMap.Geometries.Polygon p = table[i].Geometry as SharpMap.Geometries.Polygon;

                if (p != null)
                {
                    string[] fieldData = new[]
                    {
                        ((ResultSuitability)_resultSuitabilityList[i]).Name
                    };

                    List<PointD> vertices = new List<PointD>();

                    foreach (SharpMap.Geometries.Point v in p.ExteriorRing.Vertices)
                        vertices.Add(new PointD((float)v.X, (float)v.Y));

                    shpWriter.AddRecord(vertices.ToArray(), vertices.Count, fieldData);
                }
            }

            shpWriter.Close();
        }

        public void ExtractSuitabilityMaps(string dirName)
        {
            for (int i = 0; i < _computedSuitabilityList.Count; i++)
            {
                DbfFieldDesc[] fields = new[] 
                { 
                    new DbfFieldDesc { FieldName = "Suit", FieldType = DbfFieldType.Character, FieldLength = 50, RecordOffset = 1 }
                };

                ShapeFileWriter shpWriter = ShapeFileWriter.CreateWriter(dirName, ((ComputedSuitability)_computedSuitabilityList[i]).name, EGIS.ShapeFileLib.ShapeType.Polygon, fields);

                FeatureDataSet dataSet = new FeatureDataSet();
                _potentialShape.ExecuteIntersectionQuery(_potentialShape.GetExtents(), dataSet);

                FeatureDataTable table = dataSet.Tables[0];

                for (int j = 0; j < table.Count; j++)
                {
                    SharpMap.Geometries.Polygon p = table[j].Geometry as SharpMap.Geometries.Polygon;

                    if (p != null)
                    {
                        string[] fieldData = new[]
                        {
                            ((ComputedSuitability)_computedSuitabilityList[i]).suitabilityTypeList[j].ToString()
                        };

                        List<PointD> vertices = new List<PointD>();

                        foreach (SharpMap.Geometries.Point v in p.ExteriorRing.Vertices)
                            vertices.Add(new PointD((float)v.X, (float)v.Y));

                        shpWriter.AddRecord(vertices.ToArray(), vertices.Count, fieldData);
                    }
                }

                shpWriter.Close();
            }
        }

        public void GetResultAsync(List<Weight> weightList)
        {
            GetResultDelegate getResult = GetResult;
            AsyncCallback completedCallback = GetResultCompletedCallback;

            lock (_sync)
            {
                if (_isEvaluationRunning)
                {
                    throw new InvalidOperationException("Evaluation is on progress.");
                }

                AsyncOperation async = AsyncOperationManager.CreateOperation(null);
                getResult.BeginInvoke(weightList, completedCallback, async);
                _isEvaluationRunning = true;
            }

        }

        private void GetResultCompletedCallback(IAsyncResult ar)
        {
            GetResultDelegate getResult = (GetResultDelegate)((AsyncResult)ar).AsyncDelegate;
            AsyncOperation async = (AsyncOperation)ar.AsyncState;

            getResult.EndInvoke(ar);

            lock (_sync)
            {
                _isEvaluationRunning = false;
            }

            GetResultAsyncCompletedEventArgs args = new GetResultAsyncCompletedEventArgs(null, false, null, DateTime.Now);

            async.PostOperationCompleted(d => OnGetResultCompleted((GetResultAsyncCompletedEventArgs) d), args);
        }

        protected virtual void OnGetResultCompleted(GetResultAsyncCompletedEventArgs args)
        {
            if (GetResultCompleted != null)
            {
                GetResultCompleted(this, args);
            }
        }

        public void GetResult(List<Weight> weightList)
        {
            if (_hasEvaluated)
            {
                _resultSuitabilityList = new List<ISuitability>();

                for (int i = 0; i < ((ComputedSuitability)_computedSuitabilityList[0]).suitabilityTypeList.Count; i++)
                {
                    ResultSuitability resultSuitability = new ResultSuitability();
                    double tempType = (double)((ComputedSuitability)_computedSuitabilityList[0]).suitabilityTypeList[i];
                    int k = 0;
                    for (int j = 0; j < _computedSuitabilityList.Count; j++)
                    {
                        double type =
                            (double)((ComputedSuitability)_computedSuitabilityList[j]).suitabilityTypeList[i];
                        if (type * weightList[j].Value > tempType * weightList[k].Value)
                        {
                            tempType = type;
                            k = j;
                        }
                    }
                    _resultSuitabilityList.Add(
                        new ResultSuitability
                            { 
                                Name = ((ComputedSuitability)_computedSuitabilityList[k]).name
                            });
                }
                _hasResult = true;
            }
            else
            {
                Evaluate();
                GetResult(weightList);
            }
        }

        public void ReEvaluate()
        {
            _hasEvaluated = false;
            Evaluate();
        }

        public void ReCompute()
        {
            _hasPrepared = false;
            ComputeBeforeEvaluation();
        }

        private void Evaluate()
        {
            if (_hasPrepared)
            {
                if (!_hasEvaluated)
                {
                    _computedSuitabilityList = new List<ISuitability>();

                    for (int i = 0; i < _computedCriteriaList[0].suitabilities.Count; i++)
                    {
                        int[] combined =
                            new int[((ComputedSuitability)_computedCriteriaList[0].suitabilities[0]).suitabilityTypeList.Count];
                        for (int j = 0; j < _computedCriteriaList.Count; j++)
                        {
                            for (int k = 0; k < ((ComputedSuitability)_computedCriteriaList[j].suitabilities[i]).suitabilityTypeList.Count; k++)
                            {
                                combined[k] = combined[k] + (int)(((ComputedSuitability)_computedCriteriaList[j].suitabilities[i]).suitabilityTypeList[k]);
                            }
                        }

                        for (int j = 0; j < ((ComputedSuitability)_computedCriteriaList[0].suitabilities[0]).suitabilityTypeList.Count; j++)
                        {
                            combined[j] = (int)Math.Round((double)combined[j] / _computedCriteriaList.Count);
                        }
                        _computedSuitabilityList.Add(
                            new ComputedSuitability(
                                ((ComputedSuitability)_computedCriteriaList[0].suitabilities[i]).name,
                                new List<SuitabilityType>(Array.ConvertAll(combined, value => (SuitabilityType)value))
                            ));
                    }
                    _hasEvaluated = true;
                }
            }
            else
            {
                ComputeBeforeEvaluation();
                Evaluate();
            }
        }

        private void ComputeBeforeEvaluation()
        {
            if (!_hasPrepared)
            {
                _computedCriteriaList = new List<Criteria>();

                _potentialShape.Open();

                Collection<uint> potShapeIndexes =
                    _potentialShape.GetObjectIDsInView(_potentialShape.GetExtents());

                for (int i = 0; i < _criterias.Count; i++)
                {
                    switch (_criterias[i].type)
                    {
                        case CriteriaType.DISTANCE:

                            _criteriaShapeList[i].Open();

                            Collection<uint> criteriaShapeIndexes =
                                _criteriaShapeList[i].GetObjectIDsInView(_criteriaShapeList[i].GetExtents());

                            switch (_distanceType)
                            {
                                case DistanceType.POLYGON:
                                    ComputePolygonDistance(_criterias[i], i,
                                        potShapeIndexes, criteriaShapeIndexes);
                                    break;
                                case DistanceType.CENTROID:
                                    ComputeCentroidDistance(_criterias[i], i,
                                        potShapeIndexes, criteriaShapeIndexes);
                                    break;
                                case DistanceType.BOUNDINGBOX:
                                    ComputeBoundingBoxDistance(_criterias[i], i,
                                        potShapeIndexes, criteriaShapeIndexes);
                                    break;
                            }
                            break;
                        case CriteriaType.FACTOR:
                            ComputeFactor(_criterias[i], potShapeIndexes);
                            break;
                        case CriteriaType.NDVI:
                            ComputeNDVI(_criterias[i], potShapeIndexes);
                            break;
                        case CriteriaType.SLOPE:
                            ComputeSlope(_criterias[i], potShapeIndexes);
                            break;
                        case CriteriaType.TYPE:
                            ComputeType(_criterias[i], potShapeIndexes);
                            break;
                    }
                }
                _hasPrepared = true;
            }
        }

        private void ComputePolygonDistance(Criteria criteria, int idx,
            Collection<uint> potShapeIndexes, Collection<uint> criteriaShapeIndexes)
        {
            List<double> minDistanceList = new List<double>();

            foreach (var potShapeIndex in potShapeIndexes)
            {
                List<double> distanceList = new List<double>();
                Geometry potGeo =
                    (Geometry)GeometryConverter.ToNTSGeometry(_potentialShape.GetFeature(potShapeIndex).Geometry,
                    new GeometryFactory());

                foreach (var criteriaShapeIndex in criteriaShapeIndexes)
                {
                    Geometry criteriaGeo =
                        (Geometry)GeometryConverter.ToNTSGeometry(
                        _criteriaShapeList[idx].GetFeature(criteriaShapeIndex).Geometry, new GeometryFactory());

                    distanceList.Add(potGeo.Distance(criteriaGeo));
                }
                distanceList.Sort();
                minDistanceList.Add(distanceList[0]);
            }
            DetermineSuitability(criteria, minDistanceList);
        }

        private void ComputeCentroidDistance(Criteria criteria, int idx,
            Collection<uint> potShapeIndexes, Collection<uint> criteriaShapeIndexes)
        {
            List<double> minDistanceList = new List<double>();

            foreach (var potShapeIndex in potShapeIndexes)
            {
                List<double> distanceList = new List<double>();
                Geometry potGeo =
                    (Geometry)GeometryConverter.ToNTSGeometry(_potentialShape.GetFeature(potShapeIndex).Geometry,
                    new GeometryFactory());
                foreach (var criteriaShapeIndex in criteriaShapeIndexes)
                {
                    Geometry criteriaGeo =
                        (Geometry)GeometryConverter.ToNTSGeometry(
                        _criteriaShapeList[idx].GetFeature(criteriaShapeIndex).Geometry, new GeometryFactory());

                    distanceList.Add(potGeo.CentroidDistance(criteriaGeo));
                }
                distanceList.Sort();
                minDistanceList.Add(distanceList[0]);
            }
            DetermineSuitability(criteria, minDistanceList);
        }

        private void ComputeBoundingBoxDistance(Criteria criteria, int idx,
            Collection<uint> potShapeIndexes, Collection<uint> criteriaShapeIndexes)
        {
            List<double> minDistanceList = new List<double>();

            foreach (var potShapeIndex in potShapeIndexes)
            {
                List<double> distanceList = new List<double>();
                FeatureDataRow potDataRow = _potentialShape.GetFeature(potShapeIndex);
                foreach (var criteriaShapeIndex in criteriaShapeIndexes)
                {
                    distanceList.Add(
                        potDataRow.BoundingBoxDistance(_criteriaShapeList[idx].GetFeature(criteriaShapeIndex)));
                }
                distanceList.Sort();
                minDistanceList.Add(distanceList[0]);
            }
            DetermineSuitability(criteria, minDistanceList);
        }

        private void ComputeFactor(Criteria criteria, Collection<uint> potShapeIndexes)
        {
        }

        private void ComputeNDVI(Criteria criteria, Collection<uint> potShapeIndexes)
        {
            List<double> ndviList = new List<double>();
            foreach (var potShapeIndex in potShapeIndexes)
            {
                FeatureDataRow potDataRow = _potentialShape.GetFeature(potShapeIndex);
                ndviList.Add(double.Parse(potDataRow.ItemArray.GetValue(1).ToString()));
            }
            DetermineSuitability(criteria, ndviList);
        }

        private void ComputeSlope(Criteria criteria, Collection<uint> potShapeIndexes)
        {
            List<double> slopeList = new List<double>();
            foreach (var potShapeIndex in potShapeIndexes)
            {
                FeatureDataRow potDataRow = _potentialShape.GetFeature(potShapeIndex);
                slopeList.Add(double.Parse(potDataRow.ItemArray.GetValue(3).ToString()));
            }
            DetermineSuitability(criteria, slopeList);
        }

        private void ComputeType(Criteria criteria, Collection<uint> potShapeIndexes)
        {
        }

        private void DetermineSuitability(Criteria criteria, List<double> evalResults)
        {
            List<ISuitability> detSuitabilityList = new List<ISuitability>();
            foreach (var suitability in criteria.suitabilities)
            {
                SuitabilityEvaluation suitabilityEvaluation;

                if (((Suitability)suitability).asc)
                {
                    suitabilityEvaluation = AscSuitabilityEvaluation;
                }
                else
                {
                    suitabilityEvaluation = DescSuitabilityEvaluation;
                }

                detSuitabilityList.Add(new ComputedSuitability(((Suitability) suitability).name,
                                        suitabilityEvaluation(((Suitability)suitability).ranges, evalResults)));
            }

            _computedCriteriaList.Add(new Criteria(criteria.type, criteria.name, detSuitabilityList));
        }

        private List<SuitabilityType> AscSuitabilityEvaluation(Range[] ranges, List<double> results)
        {
            List<SuitabilityType> suitabilityResults = new List<SuitabilityType>();
            foreach (var result in results)
            {
                if (result > ranges[0].start && result < ranges[1].start)
                {
                    suitabilityResults.Add(ranges[0].suitability);
                }
                else if(result > ranges[1].start && result < ranges[2].start)
                {
                    suitabilityResults.Add(ranges[1].suitability);
                }
                else if (result > ranges[2].start && result < ranges[3].start)
                {
                    suitabilityResults.Add(ranges[2].suitability);
                }
                else if (result > ranges[3].start && result < ranges[4].start)
                {
                    suitabilityResults.Add(ranges[3].suitability);
                }
                else if (result > ranges[4].start)
                {
                    suitabilityResults.Add(ranges[4].suitability);
                }
            }
            return suitabilityResults;
        }

        private List<SuitabilityType> DescSuitabilityEvaluation(Range[] ranges, List<double> results)
        {
            List<SuitabilityType> suitabilityResults = new List<SuitabilityType>();
            foreach (var result in results)
            {
                if (result > ranges[0].start)
                {
                    suitabilityResults.Add(ranges[0].suitability);
                }
                else if(result > ranges[1].start && result < ranges[0].start)
                {
                    suitabilityResults.Add(ranges[1].suitability);
                }
                else if (result > ranges[2].start && result < ranges[1].start)
                {
                    suitabilityResults.Add(ranges[2].suitability);
                }
                else if (result > ranges[3].start && result < ranges[2].start)
                {
                    suitabilityResults.Add(ranges[3].suitability);
                }
                else if (result > ranges[4].start && result < ranges[3].start)
                {
                    suitabilityResults.Add(ranges[4].suitability);
                }
            }
            return suitabilityResults;
        }
    }
}